package alibaba

import (
	"context"
	"fmt"
	"regexp"
	"sort"
	"strconv"
	"strings"

	"golang.aliyun-inc.com/agit/agit/proc-receive-hook/protocol"
	log "golang.aliyun-inc.com/agit/alog/alogcore"
	pb "golang.aliyun-inc.com/agit/satellite-proto/go"
)

type CommandHandler interface {
	handleAgitFlow(ctx context.Context, command *protocol.Command) (*pb.Command, error)
	handleAgitFlowCR(ctx context.Context, command *protocol.Command) (*pb.Command, error)
	handleTrunkBase(ctx context.Context, command *protocol.Command, pushOpts map[string]string) (pbCmd *pb.Command, err error)
	handleTrunkBaseCR(ctx context.Context, command *protocol.Command, pushOpts map[string]string) (pbCmd *pb.Command, err error)
	handleRefReview(ctx context.Context, commands []*protocol.Command, message *string) (pbCmd *pb.Command, err error)
	handleNormal(command *protocol.Command) (*pb.Command, error)
}

type CodeupCommandHandler struct {
	api       API
	crAPI     CodeReviewAPI
	checker   DivergeChecker
	isMaster  bool
	pushopts  *[]protocol.PushOption
	callbacks *[]func(error)
}

var pattern = regexp.MustCompile(`<a\s+(?:[^>]*?\s+)?href=(["'])(.*?)">`)

// handleAgitFlow tries generate a pb.Command from the passed in command.
// Note we are not returning error here to avoid killing other commands, this
// should change in future when atomic support added
func (c *CodeupCommandHandler) handleAgitFlow(ctx context.Context, command *protocol.Command) (*pb.Command, error) {
	if !c.isMaster {
		return voteSkipForSlave(command)
	}

	params, err := c.resolveAGitFlowParams(ctx, command)
	if err != nil {
		log.WithError(err).Error(ctx, "resolve agitflow params failed")
		command.SetStatusNG(err.Error())
		return nil, err
	}

	var pbCmd *pb.Command
	switch params.Action {
	case "create":
		pbCmd, err = c.createNewAgitFlow(ctx, command, params)
	case "update":
		pbCmd, err = c.updateAgitFlow(ctx, command, params)
	default:
		command.SetStatusNG(fmt.Sprintf("invalid agitflow action %q", params.Action))
		return nil, fmt.Errorf("invalid agitflow action %q", params.Action)
	}

	if err != nil {
		return nil, err
	}

	if pbCmd != nil {
		pbCmd.SkipVoting = true
	}
	return pbCmd, nil
}

func (c *CodeupCommandHandler) handleAgitFlowCR(ctx context.Context, command *protocol.Command) (*pb.Command, error) {
	if !c.isMaster {
		return voteSkipForSlave(command)
	}
	params, crID, oldCommit, err := c.calculateAgitAction(ctx, command)
	if err != nil {
		command.SetStatusNG("failed to resolve params")
	}
	switch params.Action {
	case "create":
		return c.createNewCR(ctx, command, params)
	case "update":
		return c.updateCR(ctx, command, params, crID, oldCommit)
	default:
		return &pb.Command{
			Refname: []byte(command.Ref),
			OldOid:  command.OldOID,
			NewOid:  command.NewOID,
		}, nil
	}
}

func (c *CodeupCommandHandler) prepareNew(ctx context.Context, command *protocol.Command, params *AGitFlowParams) (pbCmd *pb.Command, err error) {
	defer func() {
		if err != nil {
			log.Error(ctx, "prepareNew failed", log.NewStringField("err", err.Error()))
			command.SetStatusNG("create MR failed")
		}
	}()

	var rsp *MergeRequestResp
	rsp, err = c.api.CreateMR(ctx, params)
	if err != nil {
		// Handle special error from codeup. BE CAREFUL
		if strings.HasPrefix(err.Error(), "无法完成此操作！存在进行中的合并请求") || strings.HasPrefix(err.Error(), "创建失败！代码评审已存在") {
			matches := pattern.FindAllStringSubmatch(err.Error(), -1)
			if len(matches) > 0 && len(matches[0]) > 2 {
				url := matches[0][2]
				command.SetHint(fmt.Sprintf("Merge request create failed. There exists a same merge request in progress:\n%s", url))
			}
		}
		command.SetHint(err.Error())
		command.SetStatusNG("create MR failed")
		return nil, nil
	}
	params.ID = strconv.Itoa(rsp.ID)

	// agitflow call create mr to update mr, if oldRefCommit is not empty then means it is agitflow update
	if rsp.RefOldOid != "" {
		if params.OldOID != "" && params.OldOID != rsp.RefOldOid {
			command.SetStatusNG("update MR failed")
			command.SetHint(fmt.Sprintf("Old oid do not Match! Remote MR has new commit. "+
				"To avoid overwrite, please execute:\n"+
				"    git download %d\n"+
				"to download newest code to continue. When local work is done, execute:\n"+
				"    git pr --change %d\n"+
				"to update the MR.", rsp.ID, rsp.ID))
			return nil, nil
		}
		return c.handleAgitUpdate(ctx, command, params, rsp.RefOldOid, rsp.DetailURL, rsp.ID)
	}

	c.registerCallback(func(e error) {
		updateMRStateReq := &UpdateMRStateParams{
			AGitFlowParams:  *params,
			WriteRefSuccess: true,
		}
		if e != nil {
			updateMRStateReq.WriteRefSuccess = false
			command.SetStatusNG("update-ref failed")
		}
		_, err := c.api.UpdateMRState(ctx, updateMRStateReq)
		if err != nil {
			command.SetStatusNG("update merge request state failed")
			return
		}
		command.SetHint(fmt.Sprintf("Merge request #%s has been created, please visit:\n%s", params.ID, rsp.DetailURL))
	})

	oldOid := NullSHA1
	if rsp.RefOldOid != "" {
		oldOid = rsp.RefOldOid
	}

	gitCommand := protocol.GitCommand{
		OldOID: oldOid,
		NewOID: params.NewOID,
		Ref:    fmt.Sprintf("refs/merge-requests/%d/head", rsp.ID),
	}
	err = command.SetIntendedCommand(gitCommand)
	if err != nil {
		return nil, err
	}

	return &pb.Command{
		Refname:    []byte(fmt.Sprintf("refs/merge-requests/%d/head", rsp.ID)),
		OldOid:     NullSHA1,
		NewOid:     params.NewOID,
		SkipVoting: true,
	}, nil
}

func (c *CodeupCommandHandler) handleAgitUpdate(ctx context.Context, command *protocol.Command, params *AGitFlowParams, oldRefCommit, detailURL string, mrID int) (pbCmd *pb.Command, err error) {
	c.registerCallback(func(e error) {
		if e != nil {
			command.SetStatusNG("update ref failed")
			return
		}
		params.TryUpdate = true
		_, err = c.api.CreateMR(ctx, params)
		if err != nil {
			// Handle special error from codeup. BE CAREFUL
			if strings.HasPrefix(err.Error(), "无法完成此操作！存在进行中的合并请求") {
				matches := pattern.FindAllStringSubmatch(err.Error(), -1)
				if len(matches) > 0 && len(matches[0]) > 2 {
					url := matches[0][2]
					command.SetHint(fmt.Sprintf("Merge request create failed. There exists a same merge request in progress:\n%s", url))
				}
				command.SetHint(err.Error())
			}
			command.SetStatusNG("update MR failed")
			return
		}
		command.SetHint(fmt.Sprintf("Merge request #%d has been updated, please visit:\n%s", mrID, detailURL))
	})

	gitCommand := protocol.GitCommand{
		OldOID: oldRefCommit,
		NewOID: params.NewOID,
		Ref:    fmt.Sprintf("refs/merge-requests/%d/head", mrID),
	}
	err = command.SetIntendedCommand(gitCommand)

	return &pb.Command{
		Refname:    []byte(fmt.Sprintf("refs/merge-requests/%d/head", mrID)),
		OldOid:     oldRefCommit,
		NewOid:     params.NewOID,
		SkipVoting: true,
	}, nil
}

func (c *CodeupCommandHandler) checkPermission(ctx context.Context, params *AGitFlowParams) (bool, error, *MergeRequestResp) {
	userID := params.UserID
	mrID := params.ID
	// query the MRs which tobe updated
	mrs, err := c.api.QueryMR(ctx, mrID)
	if err != nil {
		log.Error(ctx, err.Error())
		return false, fmt.Errorf("query MR failed"), nil
	}
	if len(mrs) == 0 {
		return false, fmt.Errorf("no MR found by %q", mrID), nil
	}

	mr := mrs[0]
	// If uploader is the author of the CR, permission check pass.
	if userID == strconv.Itoa(mr.Author.ID) {
		params.RefOldOid = mr.SourceBranch
		return true, nil, mr
	}
	for _, assignee := range mr.Assignee {
		if userID == strconv.Itoa(assignee.ID) {
			params.RefOldOid = mr.SourceBranch
			return true, nil, mr
		}
	}

	return false, fmt.Errorf("you need to be the author or assignee to update the mr"), nil
}

func (c *CodeupCommandHandler) checkMrStatus(ctx context.Context, mr *MergeRequestResp) (error, *MergeRequestResp) {
	if !strings.HasPrefix(mr.CreationMethod, "CLIENT") {
		return fmt.Errorf("指定的评审不是使用命令行创建的，无法使用命令行更新"), mr
	}
	if mr.State != "opened" && mr.State != "accepted" && mr.State != "reopened" {
		return fmt.Errorf("更新评审失败，请确认评审的状态是否开启中"), mr
	}
	return nil, mr
}

func (c *CodeupCommandHandler) prepareUpdate(ctx context.Context, command *protocol.Command, params *AGitFlowParams) (pbCmd *pb.Command, err error) {
	c.registerCallback(func(e error) {
		if e != nil {
			command.SetStatusNG("update-ref failed")
			return
		}
		var rsp *MergeRequestResp
		rsp, err = c.api.UpdateMR(ctx, params)

		if err != nil {
			command.SetHint(err.Error())
			command.SetStatusNG("update-ref failed")
			return
		}
		command.SetHint(fmt.Sprintf("Merge request #%d has been updated, please visit:\n%s", rsp.ID, rsp.DetailURL))
	})

	gitCommand := protocol.GitCommand{
		OldOID: params.RefOldOid,
		NewOID: params.NewOID,
		Ref:    fmt.Sprintf("refs/merge-requests/%s/head", params.ID),
	}
	err = command.SetIntendedCommand(gitCommand)
	if err != nil {
		return nil, err
	}

	return &pb.Command{
		Refname:    []byte(fmt.Sprintf("refs/merge-requests/%s/head", params.ID)),
		OldOid:     params.RefOldOid,
		NewOid:     params.NewOID,
		SkipVoting: true,
	}, nil
}

func (c *CodeupCommandHandler) handleNormal(command *protocol.Command) (*pb.Command, error) {
	c.registerCallback(func(e error) {
		if e != nil {
			command.SetStatusNG("update-ref failed")
		}
	})
	intended := command.GetIntendedCommand()
	return &pb.Command{
		Refname: []byte(intended.Ref),
		OldOid:  intended.OldOID,
		NewOid:  intended.NewOID,
	}, nil
}

func (c *CodeupCommandHandler) handleTrunkBase(ctx context.Context, command *protocol.Command, pushOpts map[string]string) (pbCmd *pb.Command, err error) {
	optReview, exist := pushOpts["review"]
	//未指定push option但还是到这里，说明仓库开了主干模式，默认smart
	if !exist {
		optReview = "smart"
	}

	if optReview == "no" {
		return c.handleNormal(command)
	}
	if !c.isMaster {
		return voteSkipForSlave(command)
	}

	defer func() {
		if err != nil {
			command.SetStatusNG(err.Error())
			err = nil
		}
	}()

	switch optReview {
	case "smart":
		pbCmd, err = c.prepareSmarter(ctx, command)
	case "new":
		pbCmd, err = c.createNewTBD(ctx, command)
	default:
		var mrID string
		var oldCommit string
		mrID, oldCommit, err = parseMrIDOldCommit(pushOpts)
		if err != nil {
			return nil, err
		}
		pbCmd, err = c.updateTBD(ctx, command, mrID, oldCommit)
	}
	return pbCmd, err
}

func (c *CodeupCommandHandler) handleTrunkBaseCR(ctx context.Context, command *protocol.Command, pushOpts map[string]string) (pbCmd *pb.Command, err error) {
	if !c.isMaster {
		return voteSkipForSlave(command)
	}
	trunkAction, crId, oldCommit, err := c.calculateTrunkAction(ctx, command, pushOpts)
	if err != nil {
		command.SetStatusNG("failed to process review push options")
		command.SetHint(err.Error())
		return nil, nil
	}
	params := &AGitFlowParams{
		OldOID:      command.OldOID,
		NewOID:      command.NewOID,
		Action:      trunkAction,
		Description: new(string),
	}
	err = c.resolveParams(ctx, params, command)
	if err != nil {
		command.SetStatusNG("failed to resolve params")
	}

	switch trunkAction {
	case "create":
		return c.createNewCR(ctx, command, params)
	case "update":
		return c.updateCR(ctx, command, params, crId, oldCommit)
	case "direct":
		return &pb.Command{
			Refname: []byte(command.Ref),
			OldOid:  command.OldOID,
			NewOid:  command.NewOID,
		}, nil
	}
	return nil, err
}

func (c *CodeupCommandHandler) registerCallback(callback func(error)) {
	*c.callbacks = append(*c.callbacks, callback)
}

func (c *CodeupCommandHandler) createNewTBD(ctx context.Context, command *protocol.Command) (*pb.Command, error) {
	params, err := c.resolveTBDParams(ctx, command, "create")
	if err != nil {
		command.SetStatusNG("resolve params failed")
		return nil, err
	}
	return c.prepareNew(ctx, command, params)
}

func (c *CodeupCommandHandler) createNewAgitFlow(ctx context.Context, command *protocol.Command, params *AGitFlowParams) (*pb.Command, error) {
	params.TryUpdate = false
	return c.prepareNew(ctx, command, params)
}

func (c *CodeupCommandHandler) updateAgitFlow(ctx context.Context, command *protocol.Command, params *AGitFlowParams) (pbCmd *pb.Command, err error) {
	ok, err, mr := c.checkPermission(ctx, params)
	if !ok || err != nil {
		command.SetStatusNG("update MR failed")
		command.SetHint(fmt.Sprintf("Merge request #%s update failed.\nYou need to be the author or reviewer to update this merge request.", params.ID))
		return nil, nil
	}
	if mr.SourceBranch != params.OldOID {
		command.SetStatusNG("update MR failed")
		command.SetHint(fmt.Sprintf("Old oid do not Match! Remote MR has new commit. "+
			"To avoid overwrite, please execute:\n"+
			"    git download %d\n"+
			"to download newest code to continue. When local work is done, execute:\n"+
			"    git pr --change %d\n"+
			"to update the MR.", mr.ID, mr.ID))
		return nil, nil
	}
	return c.prepareUpdate(ctx, command, params)
}

func (c *CodeupCommandHandler) updateTBD(ctx context.Context, command *protocol.Command, mrID string, oldCommit string) (*pb.Command, error) {
	params, err := c.resolveTBDParams(ctx, command, "update")
	if err != nil {
		return nil, fmt.Errorf("resolve params failed")
	}
	params.ID = mrID
	params.OldCommit = oldCommit
	ok, err, mr := c.checkPermission(ctx, params)
	if !ok || err != nil {
		log.Error(ctx, err.Error())
		command.SetHint(fmt.Sprintf("Merge request #%s update failed.\nYou need to be the author or reviewer to update this merge request.", mrID))
		return nil, fmt.Errorf("无权限操作MR或对应MR不存在")
	}

	err, _ = c.checkMrStatus(ctx, mr)
	if err != nil {
		return nil, err
	}

	err = c.checker.checkDiverge(ctx, command, params, mr)
	if err != nil {
		command.SetHint(err.Error())
		return nil, err
	}
	log.Info(ctx, "updateTBD", log.NewStringField("params: ", fmt.Sprintf("+%v", params)))
	return c.prepareUpdate(ctx, command, params)
}

func (c *CodeupCommandHandler) queryOpenedMRbyUserTarget(ctx context.Context, command *protocol.Command) ([]*MergeRequestResp, error) {
	params := &AGitFlowParams{}
	if err := params.resolveEnv(); err != nil {
		log.Error(ctx, "prepareSmart resolve env failed", log.NewStringField("err", err.Error()))
		return nil, err
	}
	if err := params.resolveSourceAndTargetBranch(command.GitCommand.Ref); err != nil {
		log.Error(ctx, "prepareSmart resolve source and target branch failed", log.NewStringField("err", err.Error()))
		return nil, err
	}

	mrs, err := c.api.QueryMRs(ctx, params.ProjectID, params.UserID, params.TargetBranch)
	if err != nil {
		log.Error(ctx, "call QueryMRs failed", log.NewStringField("err", err.Error()))
		return nil, fmt.Errorf("query MR failed")
	}
	return mrs, nil
}

func (c *CodeupCommandHandler) prepareSmarter(ctx context.Context, command *protocol.Command) (pbCmd *pb.Command, err error) {
	mrs, err := c.queryOpenedMRbyUserTarget(ctx, command)
	switch len(mrs) {
	case 0:
		return c.createNewTBD(ctx, command)
	case 1:
		return c.updateTBD(ctx, command, strconv.Itoa(mrs[0].ID), "")
	default:
		return c.guessByBestEffort(ctx, command, mrs)
	}
}

func (c *CodeupCommandHandler) guessByBestEffort(ctx context.Context, command *protocol.Command, mrs mrList) (pbCmd *pb.Command, err error) {
	sort.Sort(mrs)
	var sb strings.Builder
	sb.WriteString("You need to manually specify the merge request to update because there are already\nmultiple merge requests that you created earlier for the same target branch:\n\n")
	for _, mr := range mrs {
		sb.WriteString(fmt.Sprintf("* [ID: %d] %s\n", mr.ID, mr.DetailURL))
	}
	sb.WriteString("\nThen, update specific merge request based on it's ID (MR-ID):\n")
	sb.WriteString("    git push -o review=<MR-ID>\n")
	sb.WriteString("\nOr you can create a new one:\n")
	sb.WriteString("    git push -o review=new")
	command.SetHint(sb.String())
	command.SetStatusNG("unable to determine which mr to update")
	return nil, nil
}

// calculateTrunkAction 计算推送评审模式要执行的动作
func (c *CodeupCommandHandler) calculateTrunkAction(ctx context.Context, command *protocol.Command, pushOpts map[string]string) (action, crId, oldCommit string, err error) {
	reviewOpt, exist := pushOpts["review"]
	//未指定push option但还是到这里，说明仓库开了主干模式，默认smart
	if !exist {
		reviewOpt = "smart"
	}
	switch reviewOpt {
	case "no":
		action = "direct"
	case "smart":
		action, crId, oldCommit, err = c.calculateSmartAction(ctx, command, pushOpts)
	case "new":
		action = "create"
	default:
		crId, oldCommit, err = parseMrIDOldCommit(pushOpts)
		if err != nil {
			return "", "", "", err
		}
		action = "update"
	}
	return action, crId, oldCommit, err
}

// calculateSmartAction 计算使用smart选项时，创建还是更新cr
func (c *CodeupCommandHandler) calculateSmartAction(ctx context.Context, command *protocol.Command, pushOpts map[string]string) (action, crId, oldCommit string, err error) {
	params := &AGitFlowParams{}
	if err := params.resolveEnv(); err != nil {
		log.Error(ctx, "resolve env failed", log.NewStringField("err", err.Error()))
		return "", "", "", err
	}
	if err := params.resolveSourceAndTargetBranch(command.GitCommand.Ref); err != nil {
		log.Error(ctx, "resolve source and target branch failed", log.NewStringField("err", err.Error()))
		return "", "", "", err
	}
	crList, err := c.crAPI.QueryCRs(ctx, &QueryCodeReviewReq{
		ProjectID:    params.ProjectID,
		AuthorID:     params.UserID,
		TargetBranch: params.TargetBranch,
		LocalBranch:  params.SourceBranch,
	})
	switch len(crList) {
	case 0:
		return "create", "", "", nil
	case 1:
		return "update", strconv.FormatInt(crList[0].LocalID, 10), "", nil
	default:
		command.SetStatusNG("unable to determine which code review to update")
		return "", "", "", fmt.Errorf(c.buildGuessMessage(crList))
	}
}

func (c *CodeupCommandHandler) calculateAgitAction(ctx context.Context, command *protocol.Command) (params *AGitFlowParams, crID, oldCommit string, err error) {
	var crs []*CodeReviewBasicInfo
	params = &AGitFlowParams{
		OldOID:      command.OldOID,
		NewOID:      command.NewOID,
		Description: new(string),
	}
	if err = params.resolveCommand(command); err != nil {
		command.SetHint("resolve command failed")
		return
	}
	err = c.resolveParams(ctx, params, command)
	if err != nil {
		return
	}
	if params.Action == "update" {
		crs, err = c.crAPI.QueryCR(ctx, params.ProjectID, params.ID)
		if err != nil {
			command.SetHint(fmt.Sprintf("query code review %s failed", params.ID))
			return
		}
		if len(crs) == 0 {
			command.SetHint(fmt.Sprintf("code review %s not found", params.ID))
		}
		crID = fmt.Sprintf("%d", crs[0].LocalID)
		return
	}
	if params.Action == "create" {
		crs, err = c.crAPI.QueryCRs(ctx, &QueryCodeReviewReq{
			ProjectID:    params.ProjectID,
			AuthorID:     params.UserID,
			TargetBranch: params.TargetBranch,
			LocalBranch:  params.SourceBranch,
		})
		if err != nil {
			command.SetHint(fmt.Sprintf("query code review of target %s failed", params.TargetBranch))
			return
		}
		if len(crs) > 0 {
			params.Action = "update"
			crID = fmt.Sprintf("%d", crs[0].LocalID)
		}
		return
	}
	err = fmt.Errorf("wrong agit action")
	return
}

func (c *CodeupCommandHandler) buildGuessMessage(crs crList) string {
	sort.Sort(crs)
	var sb strings.Builder
	//只显示5个
	limit := 5
	if len(crs) < limit {
		limit = len(crs)
	}
	sb.WriteString("You need to manually specify the code review to update because there are already\nmultiple code reviews that you created earlier for the same target branch:\n\n")
	for i := 0; i < limit; i++ {
		sb.WriteString(fmt.Sprintf("* [ID: %d] %s\n", crs[i].LocalID, crs[i].DetailUrl))
	}
	if len(crs) > 5 {
		sb.WriteString(fmt.Sprintf("... remain %d code reviews are ommited\n", len(crs)-5))
	}
	sb.WriteString("\nThen, update specific code review based on it's ID (CR-ID):\n")
	sb.WriteString("    git push -o review=<CR-ID>\n")
	sb.WriteString("\nOr you can create a new one:\n")
	sb.WriteString("    git push -o review=new")
	return sb.String()
}

func (c *CodeupCommandHandler) createNewCR(ctx context.Context, command *protocol.Command, params *AGitFlowParams) (pbCmd *pb.Command, err error) {
	if !c.isMaster {
		return voteSkipForSlave(command)
	}
	defer func() {
		if err != nil {
			log.Error(ctx, "createNewCR failed", log.NewStringField("err", err.Error()))
			command.SetStatusNG("create code review failed")
			err = nil
		}
	}()

	request := convert2CrRequest(params)
	localID, err := c.crAPI.GenerateLocalID(ctx, request.TargetProjectID)
	if err != nil {
		log.Error(ctx, "GenerateLocalID failed", log.NewStringField("err", err.Error()))
		return
	}
	request.LocalID = localID
	request.SourceRef = fmt.Sprintf("refs/changes/%d/head", localID)

	c.registerCallback(func(e error) {
		if e != nil {
			command.SetStatusNG("update-ref failed")
			return
		}
		rsp, err := c.crAPI.CreateCR(ctx, request)
		if err != nil {
			command.SetStatusNG("update-ref failed")
			command.SetHint("call create code review API failed")
			return
		}
		command.SetHint(fmt.Sprintf("Code review #%d has been created, please visit:\n%s", rsp.LocalID, rsp.DetailUrl))
	})

	gitCommand := protocol.GitCommand{
		OldOID: NullSHA1,
		NewOID: command.NewOID,
		Ref:    fmt.Sprintf("refs/changes/%d/head", localID),
	}
	err = command.SetIntendedCommand(gitCommand)
	if err != nil {
		command.SetHint(err.Error())
		return nil, err
	}

	return &pb.Command{
		Refname:    []byte(fmt.Sprintf("refs/changes/%d/head", localID)),
		OldOid:     NullSHA1,
		NewOid:     command.NewOID,
		SkipVoting: true,
	}, nil
}

func (c *CodeupCommandHandler) updateCR(ctx context.Context, command *protocol.Command, params *AGitFlowParams, crID string, oldCommit string) (pbCmd *pb.Command, err error) {
	if !c.isMaster {
		return voteSkipForSlave(command)
	}
	defer func() {
		if err != nil {
			log.Error(ctx, "update cr failed",
				log.NewStringField("local-id", crID),
				log.NewStringField("oldCommit", oldCommit),
				log.NewStringField("err", err.Error()))
			command.SetStatusNG("update code review failed")
			err = nil
		}
	}()

	basicInfo, err := c.prepareUpdateCR(ctx, command, params, crID, oldCommit)
	if err != nil {
		return nil, err
	}
	if basicInfo.SourceCommitID == params.NewOID {
		command.SetHint(fmt.Sprintf("no commits to update for code review #%d", basicInfo.LocalID))
		command.SetStatusNG("everything up-to-date")
		return nil, nil
	} else {
		request := convert2CrRequest(params)
		request.LocalID = basicInfo.LocalID

		c.registerCallback(func(e error) {
			if e != nil {
				command.SetStatusNG("update-ref failed")
			}

			rsp, err := c.crAPI.UpdateCR(ctx, request)
			if err != nil {
				command.SetHint("update ref success, but call update code review API failed, please try again")
				command.SetStatus(protocol.CommandStatusOK, "call code review API failed")
				return
			}
			command.SetHint(fmt.Sprintf("Code review #%d has been updated, please visit:\n%s", rsp.LocalID, rsp.DetailUrl))
		})

		gitCommand := protocol.GitCommand{
			OldOID: basicInfo.SourceCommitID,
			NewOID: command.NewOID,
			Ref:    fmt.Sprintf("refs/changes/%s/head", crID),
		}
		err = command.SetIntendedCommand(gitCommand)
		if err != nil {
			command.SetHint(err.Error())
			return nil, err
		}
	}

	return &pb.Command{
		Refname:    []byte(fmt.Sprintf("refs/changes/%s/head", crID)),
		OldOid:     basicInfo.SourceCommitID,
		NewOid:     command.NewOID,
		SkipVoting: true,
	}, nil
}

// prepareUpdateCR 查询cr，并做一些检查，是否会偏离，是否同一用户等等
func (c *CodeupCommandHandler) prepareUpdateCR(ctx context.Context, command *protocol.Command, params *AGitFlowParams, crID string, oldCommit string) (cr *CodeReviewBasicInfo, err error) {
	patch := &CodeReviewPatchSet{}
	crs, err := c.crAPI.QueryCR(ctx, params.ProjectID, crID)
	if err != nil {
		command.SetHint("call query code review api failed")
		return
	}
	if len(crs) <= 0 {
		err = fmt.Errorf("code review %s not found", crID)
		command.SetHint(err.Error())
		return
	}
	cr = crs[0]
	patch, err = c.crAPI.QueryPatch(ctx, params.ProjectID, crID)
	if err != nil {
		command.SetHint(fmt.Sprintf("query latest patch for code review %s failed", crID))
		return
	}
	if err = c.checker.checkCR(ctx, command, cr, patch, params, oldCommit); err != nil {
		command.SetHint(err.Error())
		return
	}

	return cr, nil
}

// handleRefReview 处理引用改动，并创建评审
func (c *CodeupCommandHandler) handleRefReview(ctx context.Context, commands []*protocol.Command, message *string) (pbCmd *pb.Command, err error) {
	if len(commands) == 0 {
		return nil, nil
	}
	var request *RefReviewRequest
	if !c.isMaster {
		return voteSkipForSlave(commands[0])
	}

	params := &AGitFlowParams{}
	if err = params.resolveEnv(); err != nil {
		return nil, fmt.Errorf("resolve env error")
	}
	if err = c.checkUser(ctx, params); err != nil {
		return
	}

	if request, err = c.buildRefRequest(ctx, params, commands); err != nil {
		return
	}

	rsp, err := c.crAPI.CreateRefCR(ctx, request)
	if err != nil {
		return nil, err
	}
	*message = rsp.DetailUrl
	return &pb.Command{
		Refname:    []byte(fmt.Sprintf("refs/changes/%d/head", request.LocalID)),
		OldOid:     rsp.SourceCommitID,
		NewOid:     rsp.SourceCommitID,
		SkipVoting: true,
	}, nil

}

func (c *CodeupCommandHandler) buildRefRequest(ctx context.Context, params *AGitFlowParams, commands []*protocol.Command) (request *RefReviewRequest, err error) {
	localID, err := c.crAPI.GenerateLocalID(ctx, params.ProjectID)
	if err != nil {
		return nil, fmt.Errorf("generate local ID failed")
	}
	var commandsDto []*Command
	for _, cmd := range commands {
		dto := &Command{
			OldOID: cmd.OldOID,
			NewOID: cmd.NewOID,
			Ref:    cmd.Ref,
		}
		commandsDto = append(commandsDto, dto)
	}

	request = &RefReviewRequest{
		LocalID:         localID,
		SourceProjectID: params.ProjectID,
		SourceRef:       fmt.Sprintf("refs/changes/%d/head", localID),
		TargetProjectID: params.ProjectID,
		CreateFrom:      ClientCreateMethod,
		AuthorID:        params.UserID,
		UserID:          params.UserID,
		Commands:        commandsDto,
	}
	return request, nil
}
